Skip to content

Implement Microsoft.PowerShell.SecretManagement extension#1074

Open
Gijsreyn wants to merge 4 commits intoPowerShell:mainfrom
Gijsreyn:feature/microsoft-powershell-secretmanagement
Open

Implement Microsoft.PowerShell.SecretManagement extension#1074
Gijsreyn wants to merge 4 commits intoPowerShell:mainfrom
Gijsreyn:feature/microsoft-powershell-secretmanagement

Conversation

@Gijsreyn
Copy link
Collaborator

PR Summary

This pull request adds the Microsoft.PowerShell.SecretManagement extension.

PR Context

@Gijsreyn
Copy link
Collaborator Author

@SteveL-MSFT Testing out the changes, I encountered the fact that most secret vault require some pre-authentication. In this case, it spawns another process and mentions:

2025-08-22T07:28:15.409628Z TRACE dsc_lib::dscresources::command_resource: 935: PID 14876: Get-Secret: C:\source\DSCv3\bin\debug\microsoft.powershell.secret.ps1:22:15
2025-08-22T07:28:15.409779Z TRACE dsc_lib::dscresources::command_resource: 935: PID 14876: Line |
2025-08-22T07:28:15.409804Z TRACE dsc_lib::dscresources::command_resource: 935: PID 14876: 22 | $secret = Get-Secret @secretParams -ErrorAction Ignore
2025-08-22T07:28:15.409945Z TRACE dsc_lib::dscresources::command_resource: 935: PID 14876: | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2025-08-22T07:28:15.410091Z TRACE dsc_lib::dscresources::command_resource: 935: PID 14876: | A valid password is required to access the Microsoft.PowerShell.SecretStore vault. Use the Unlock-SecretStore cmdlet to provide the required password to access the store.

Does it make sense to add an additional argument to be passed along as secureString in the config and use Unlock-SecretStore from the script? Or are there other ideas?

@Gijsreyn Gijsreyn force-pushed the feature/microsoft-powershell-secretmanagement branch from fe1951b to 1d6d6ba Compare August 25, 2025 11:25
@michaeltlombardi
Copy link
Collaborator

I'm not sure I can think of a clean way to do this from within the existing data model for a configuration document to avoid requiring the user to perform initialization steps and documenting those.

Arguably, we could specify something like extensions as a field in Microsoft.DSC under the metadata section of the configuration document, where you can provide options for the extension (such as passing it the credential required to unlock a vault). This would require more in-depth thinking around how extensions should surface their options/behaviors so we can expose those coherently to the user.

The following snippet is just an example, not fully thought out:

$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
metadata:
  Microsoft.DSC:
    extensions: # map where keys must be the extension type
      Microsoft.PowerShell/SecretManagement:
        enabled: true # default, users can explicitly disable
        version: 1.2  # optionally pin to specific version
        # Other DSC-specific stuff goes at the top-level of the map,
        # so we can do validation/interpretation.
        # 
        # Everything under options comes from the extension, which
        # probably has to publish them as a JSON Schema in the manifest.
        options:      # map of options to pass to the extension
          unlockCredential: "[parameters('secretStoreCred')]"

This would enable in-document control / options for extensions. If we require extensions to publish their options as a JSON Schema, we can also use this model for incorporating extension options into your DSC settings/policy. I think we probably need to resolve extension options sooner than later, but I wouldn't block this PR on it.

Probably, the correct (current) model would be to document that this extension requires you to invoke DSC from a PowerShell session where you have already used the Unlock-SecretStore command before invoking DSC. Maybe the extension could handle this case as a known semantic exit code and surface a useful error message to the user when the vault isn't already unlocked.

@michaeltlombardi
Copy link
Collaborator

I filed #1080 for handling extension options separately from this PR.

@Gijsreyn Gijsreyn force-pushed the feature/microsoft-powershell-secretmanagement branch from 5f01d9b to 85cbaf5 Compare August 27, 2025 00:36
@Gijsreyn
Copy link
Collaborator Author

Thanks, Mikey, for always providing well-explained and possible solutions to tackle the issue. Let's keep it open for now and see what the others think about it :)

…into feature/microsoft-powershell-secretmanagement
Copilot AI review requested due to automatic review settings March 23, 2026 00:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a PowerShell-based DSC secret extension that retrieves values via the Microsoft.PowerShell.SecretManagement module, plus a Pester test validating end-to-end secret resolution through dsc config get.

Changes:

  • Added a SecretManagement-backed secret extension manifest and retrieval script (pwsh + Get-Secret).
  • Added a Pester test that installs SecretManagement/SecretStore and validates [secret('...')] in a config.
  • Added copy_files.txt to include extension files during packaging (though packaging wiring appears incomplete).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
extensions/powershell/secret/microsoft.powershell.secret.tests.ps1 Adds an end-to-end Pester test that provisions SecretStore and validates DSC secret expansion.
extensions/powershell/secret/microsoft.powershell.secret.ps1 Implements the secret lookup script using Get-Secret -AsPlainText.
extensions/powershell/secret/microsoft.powershell.dsc.extension.json Declares the extension manifest for Microsoft.PowerShell/SecretManagement.
extensions/powershell/secret/copy_files.txt Declares files to copy into build outputs for this extension.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
}

$env:PSModulePath += [System.IO.Path]::PathSeparator + $TestDrive
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$env:PSModulePath is modified but never restored. This can leak state into later Pester suites in the same run; please capture the original value in BeforeAll and restore it in AfterAll (similar to how other tests restore $env:PATH).

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +25
# Instead of doing it in the BeforeAll block, reset the store here as we know we are running in the CI
Reset-SecretStore -Password (ConvertTo-SecureString -AsPlainText -String 'P@ssw0rd' -Force) -Force
Register-SecretVault -Name 'VaultA' -ModuleName 'Microsoft.PowerShell.SecretStore' -DefaultVault
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reset-SecretStore resets the current user’s SecretStore and can delete/overwrite existing secrets. If this suite is intended for CI-only execution, please ensure it is skipped locally; otherwise, consider using an isolated/temporary store configuration so running Invoke-Pester doesn’t destroy a developer’s SecretStore state.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +24
$secret = Get-Secret @secretParams -ErrorAction Ignore

Write-Output $secret
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Get-Secret isn’t available (module missing) or Get-Secret fails for reasons other than “not found”, this script silently produces no output due to the Get-Command guard and -ErrorAction Ignore. That can surface as a misleading “Secret '' not found” from DSC. Consider emitting a clear error to stderr and returning a non-zero exit code when prerequisites are missing or when Get-Secret errors unexpectedly, while still returning no output for the “secret not found” case.

Suggested change
$secret = Get-Secret @secretParams -ErrorAction Ignore
Write-Output $secret
try {
$secret = Get-Secret @secretParams -ErrorAction Stop
Write-Output $secret
}
catch {
$errorRecord = $_
$exceptionTypeName = $errorRecord.Exception.GetType().Name
$fullyQualifiedErrorId = $errorRecord.FullyQualifiedErrorId
# Treat "secret not found" as a non-error and return no output to preserve existing behavior.
if ($exceptionTypeName -eq 'SecretNotFoundException' -or
$fullyQualifiedErrorId -like '*SecretNotFound*') {
return
}
Write-Error -Message ("Failed to retrieve secret '{0}': {1}" -f $Name, $errorRecord.Exception.Message)
exit 1
}
}
else {
Write-Error -Message "Required cmdlet 'Get-Secret' was not found. Ensure the Microsoft.PowerShell.SecretManagement module is installed and imported."
exit 1

Copilot uses AI. Check for mistakes.
$secretParams['Vault'] = $Vault
}

$secret = Get-Secret @secretParams -ErrorAction Ignore
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace after -ErrorAction Ignore can cause noisy diffs/lint failures in some pipelines; please remove the extra space at end of line.

Suggested change
$secret = Get-Secret @secretParams -ErrorAction Ignore
$secret = Get-Secret @secretParams -ErrorAction Ignore

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,2 @@
microsoft.powershell.secret.ps1
microsoft.powershell.dsc.extension.json
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding copy_files.txt suggests these files should be included in build/package outputs, but packaging.ps1 currently only builds/copies extensions/appx under the extensions/ tree. Without adding extensions/powershell/secret to the build/packaging project list (or otherwise wiring this directory into packaging), these files likely won’t ship in produced artifacts.

Suggested change
microsoft.powershell.dsc.extension.json

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
BeforeDiscovery {
$runningInCI = $false
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$runningInCI is hard-coded to $false, but the Describe block uses -Skip:($runningInCI), so these tests will always run (including on developer machines). Because the test later calls Reset-SecretStore, this can wipe a developer’s existing SecretStore; please detect CI via environment variables (e.g., GITHUB_RUN_ID/TF_BUILD) and gate the suite appropriately (or remove the destructive calls outside CI).

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
foreach ($module in $FullyQualifiedName) {
if (-not (Get-Module -ListAvailable -FullyQualifiedName $module)) {
Save-PSResource -Name $module.ModuleName -Version $module.ModuleVersion -Path $TestDrive -Repository PSGallery -TrustRepository
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test downloads modules directly from PSGallery. In this repo’s CI, packaging.ps1 switches to the internal CFS mirror when $env:TF_BUILD is set; using PSGallery here can fail in ADO/offline environments. Consider selecting the repository dynamically (and registering CFS when needed) to match the build/test pipeline.

Suggested change
foreach ($module in $FullyQualifiedName) {
if (-not (Get-Module -ListAvailable -FullyQualifiedName $module)) {
Save-PSResource -Name $module.ModuleName -Version $module.ModuleVersion -Path $TestDrive -Repository PSGallery -TrustRepository
# Select repository dynamically to align with CI configuration
$repositoryName = 'PSGallery'
if ($env:TF_BUILD -and (Get-Command -Name Get-PSResourceRepository -ErrorAction SilentlyContinue)) {
$cfsRepo = Get-PSResourceRepository -Name 'CFS' -ErrorAction SilentlyContinue
if ($null -ne $cfsRepo) {
$repositoryName = 'CFS'
}
}
foreach ($module in $FullyQualifiedName) {
if (-not (Get-Module -ListAvailable -FullyQualifiedName $module)) {
Save-PSResource -Name $module.ModuleName -Version $module.ModuleVersion -Path $TestDrive -Repository $repositoryName -TrustRepository

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants